/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal.generator;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.sourceprojects.lycia.ConstraintValidator;
import org.sourceprojects.lycia.annotations.Child;
import org.sourceprojects.lycia.annotations.Parent;
import org.sourceprojects.lycia.annotations.Tag;
import org.sourceprojects.lycia.internal.validation.NotNullConstraintValidator;
import org.w3c.dom.Element;

/**
 * This class is a port of Apache BeanValidations TypeUtils which is really a
 * great class :) Thanks to the authors for a lot of great ideas how to handle
 * Types correct.
 * 
 * @author noctarius
 * 
 */
public class TypeUtils {

	private static final Map<Class<?>, Set<Class<?>>> PRIMITIVE_SUBTYPES;

	// Statically initialize "subtypes" of primitive values
	static {
		final Map<Class<?>, Set<Class<?>>> primitiveSubtypes = new HashMap<Class<?>, Set<Class<?>>>();

		putPrimitiveSubtypes(primitiveSubtypes, Void.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Boolean.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Byte.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Character.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Short.TYPE, Byte.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Integer.TYPE, Character.TYPE,
				Short.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Long.TYPE, Integer.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Float.TYPE, Long.TYPE);
		putPrimitiveSubtypes(primitiveSubtypes, Double.TYPE, Float.TYPE);

		PRIMITIVE_SUBTYPES = primitiveSubtypes;
	}

	private TypeUtils() {
	}

	public static final Type getComponentType(final Type type) {
		if (type instanceof Class<?>) {
			Class<?> clazz = (Class<?>) type;
			return clazz.isArray() ? clazz.getComponentType() : null;
		} else if (type instanceof GenericArrayType) {
			return ((GenericArrayType) type).getGenericComponentType();
		}

		return null;
	}

	private static final void putPrimitiveSubtypes(
			final Map<Class<?>, Set<Class<?>>> primitiveSubtypes,
			final Class<?> primitiveType, final Class<?>... directSubtypes) {

		final Set<Class<?>> subtypes = new HashSet<Class<?>>();

		for (Class<?> directSubtype : directSubtypes) {
			subtypes.add(directSubtype);
			subtypes.addAll(primitiveSubtypes.get(directSubtype));
		}

		primitiveSubtypes.put(primitiveType, subtypes);
	}

	private static final Type getArrayType(final Type componentType) {
		if (componentType instanceof Class<?>) {
			return getClassArrayType((Class<?>) componentType);
		}

		return getGenericArrayType(componentType);
	}

	private static final Type getGenericArrayType(final Type componentType) {
		return new GenericArrayType() {

			@Override
			public Type getGenericComponentType() {
				return componentType;
			}

			@Override
			public int hashCode() {
				return componentType.hashCode();
			}

			@Override
			public boolean equals(final Object object) {
				return object instanceof GenericArrayType
						&& componentType.equals(((GenericArrayType) object)
								.getGenericComponentType());
			}
		};
	}

	private static final Class<?> getClassArrayType(final Class<?> componentType) {
		return Array.newInstance(componentType, 0).getClass();
	}

	public static Type resolveTypes(final Map<Type, Type> resolvedTypes,
			final Type type, final Class<?> resolveType) {

		if (type == null) {
			return null;

		} else if (type instanceof Class<?>) {
			Class<?> clazz = (Class<?>) type;
			final Type returnedType = resolveTypeForHierarchy(resolvedTypes,
					clazz, resolveType);

			if (returnedType != null) {
				return returnedType;
			}

		} else if (type instanceof ParameterizedType) {
			ParameterizedType paramType = (ParameterizedType) type;

			if (!(paramType.getRawType() instanceof Class<?>)) {
				return null;
			}

			Class<?> rawType = (Class<?>) paramType.getRawType();

			TypeVariable<?>[] originalTypes = rawType.getTypeParameters();
			Type[] partiallyResolvedTypes = paramType.getActualTypeArguments();

			int nbrOfParams = originalTypes.length;
			for (int i = 0; i < nbrOfParams; i++) {
				resolvedTypes.put(originalTypes[i], partiallyResolvedTypes[i]);
			}

			if (rawType.equals(resolveType)) {
				return type;

			} else {
				Type returnedType = resolveTypeForHierarchy(resolvedTypes,
						rawType, resolveType);

				if (returnedType != null) {
					return returnedType;
				}
			}
		}
		return null;
	}

	private static Type resolveTypeForHierarchy(
			final Map<Type, Type> resolvedTypes, final Class<?> clazz,
			final Class<?> resolveType) {

		Type returnedType = resolveTypes(resolvedTypes, clazz
				.getGenericSuperclass(), resolveType);

		if (returnedType != null) {
			return returnedType;
		}

		for (Type genericInterface : clazz.getGenericInterfaces()) {
			returnedType = resolveTypes(resolvedTypes, genericInterface,
					resolveType);

			if (returnedType != null) {
				return returnedType;
			}
		}

		return null;
	}

	private static Class<?> getErasedReferenceType(Type type) {
		return (Class<?>) getErasedType(type);
	}

	private static boolean isArray(Type type) {
		return (type instanceof Class<?> && ((Class<?>) type).isArray())
				|| (type instanceof GenericArrayType);
	}

	private static Type getErasedType(Type type) {
		if (type instanceof ParameterizedType) {
			Type rawType = ((ParameterizedType) type).getRawType();

			return getErasedType(rawType);
		}

		if (isArray(type)) {
			Type componentType = getComponentType(type);
			Type erasedComponentType = getErasedType(componentType);

			return getArrayType(erasedComponentType);
		}

		if (type instanceof TypeVariable<?>) {
			Type[] bounds = ((TypeVariable<?>) type).getBounds();

			return getErasedType(bounds[0]);
		}

		return type;
	}

	public static void main(String[] args) throws SecurityException,
			NoSuchMethodException {
		Method method = TypeUtils.class.getDeclaredMethod("foo", List.class);

		Class<?> clazz = method.getParameterTypes()[0];

		Map<Type, Type> resolvedTypes = new HashMap<Type, Type>();
		Type type = resolveTypes(resolvedTypes, clazz, List.class);

		System.out.println(clazz);
		System.out.println(type);
		System.out.println(type instanceof ParameterizedType);

		if (type instanceof ParameterizedType) {
			ParameterizedType pType = (ParameterizedType) type;

			TypeVariable<?>[] typeVariables = getErasedReferenceType(pType)
					.getTypeParameters();
			Type[] types = pType.getActualTypeArguments();

			for (int i = 0; i < types.length; i++) {
				System.out.println("Type: " + types[i] + " => "
						+ typeVariables[i]);
			}
		}

		for (Entry<Type, Type> entry : resolvedTypes.entrySet()) {
			System.out.println("Key: " + entry.getKey() + " => "
					+ entry.getValue() + "("
					+ (entry.getKey() instanceof ParameterizedType) + "::"
					+ (entry.getValue() instanceof ParameterizedType) + ")");
		}
	}

	private static void foo(
			@Child(value = @Tag(""), listType = Element.class) List<Element> elements) {

	}

}
